/*
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.common.objectpool;

import java.lang.reflect.Array;

import com.facebook.common.logging.FLog;
import com.facebook.common.time.MonotonicClock;

/**
 * Generic object pool implementation for arbitrary types.  Provides an interface for performing
 * actions when allocating from / releasing to the pool, as well as a simple compaction strategy
 * based upon the last time we needed the pool to be as large as it is.
 *
 * @param <T> type of the object to pool
 */
public class ObjectPool<T> {

    private static final Class<?> TAG = ObjectPool.class;

    private final Class<T> mClazz;
    // minimum and maximum size for the pool
    private final int mMinSize;
    private final int mMaxSize;

    // increment size the pool size is increased by when there is not enough space
    private final int mIncrementSize;
    private final Allocator<T> mAllocator;

    private final MonotonicClock mClock;

    // amount of time in ms since the last 'low capacity' event before we reduce the size of the pool
    private final long mCompactionDelayMs;

    // The last time we had low supply (denoted as less than 2x the increment size in spare capacity)
    private long mLastLowSupplyTimeMs;

    private T[] mPool;
    private int mSize;

    /**
     * Generic allocator interface for the pool.  Encapsulates the per-object logic needed to
     * instantiate, destroy, and do operations when an object is allocated from / returned to the pool
     * The call sequence for a given object will look like:
     * obj1 = Allocator.create()
     * [
     * Allocator.onAllocate(obj1)
     * Allocator.onRelease(obj1)
     * ] (zero or more times)
     * Allocator.onDestroy(obj1) // if the object is freed from the pool
     *
     * @param <T>
     */
    public interface Allocator<T> {

        /**
         * Create and return a new instance of T
         *
         * @return new instance of T suitable for pooling
         */
        public T create();

        /**
         * Handler for when an instance of T is allocated from the pool
         *
         * @param obj instance of T that is being allocated
         */
        public void onAllocate(T obj);

        /**
         * Handler for when an instance of T is returned to the pool
         *
         * @param obj instance of T that is being returned
         */
        public void onRelease(T obj);

    }

    /**
     * Basic implementation of an Allocator.  Uses new to create the object instance
     *
     * @param <T>
     */
    public static class BasicAllocator<T> implements Allocator<T> {

        Class<T> mClazz;

        public BasicAllocator(Class<T> clazz) {
            mClazz = clazz;
        }

        @Override
        public T create() {
            try {
                return mClazz.newInstance();
            } catch (InstantiationException e) {
                FLog.e(TAG, "Couldn't instantiate object", e);
            } catch (IllegalAccessException e) {
                FLog.e(TAG, "Couldn't instantiate object", e);
            }
            return null;
        }

        @Override
        public void onAllocate(T obj) {
        }

        @Override
        public void onRelease(T obj) {
        }
    }

    @SuppressWarnings("unchecked")
    public ObjectPool(Class<T> clazz, int minSize, int maxSize, int incrementSize,
                      long compactionDelay, Allocator<T> alloc, MonotonicClock clock) {
        mClazz = clazz;
        mMinSize = Math.max(minSize, 0);
        mMaxSize = Math.max(mMinSize, maxSize);
        mIncrementSize = Math.max(incrementSize, 1);
        mCompactionDelayMs = compactionDelay;
        mAllocator = alloc;
        mClock = clock;
        mPool = (T[]) Array.newInstance(mClazz, mMinSize);
    }

    /**
     * Return a free instance of T for use.  Tries to return an already allocated instance, or creates
     * a new one if none exist
     *
     * @return an instance of T for use
     */
    public synchronized T allocate() {
        T obj;
        if (mSize > 0) {
            --mSize;
            obj = mPool[mSize];
            mPool[mSize] = null;
        } else {
            obj = mAllocator.create();
        }
        mAllocator.onAllocate(obj);
        return obj;
    }

    /**
     * Return a previously allocated object back to the pool
     *
     * @param obj object to return to the pool
     */
    public synchronized void release(T obj) {
        // Check usage at the beginning of release since it represents the low point
        checkUsage();

        mAllocator.onRelease(obj);
        if (mSize < mMaxSize) {
            if (mSize + 1 > mPool.length) {
                resizePool(Math.min(mMaxSize, mPool.length + mIncrementSize));
            }
            mPool[mSize++] = obj;
        }
    }

    /**
     * Checks whether the compaction policies set for this pool have been satisfied
     */
    public synchronized void checkUsage() {
        final long now = mClock.now();

        // this check prevents compaction from occurring by setting the last timestamp
        // to right now when the size is less than 2x the increment size (default
        // ObjectPoolBuilder.DEFAULT_INCREMENT_SIZE).
        if (mSize < 2 * mIncrementSize) {
            mLastLowSupplyTimeMs = now;
        }

        if (now - mLastLowSupplyTimeMs > mCompactionDelayMs) {
            FLog.d(TAG, "ObjectPool.checkUsage is compacting the pool.");
            compactUsage();
        }
    }

    /**
     * Forcibly compacts the pool by the set increment size, regardless of the compaction policy.
     */
    public synchronized void compactUsage() {
        int newSize = Math.max(mPool.length - mIncrementSize, mMinSize);
        if (newSize != mPool.length) {
            resizePool(newSize);
        }
    }

    /**
     * Get the number of free objects currently in the pool.
     *
     * @return the number of free objects
     */
    public int getPooledObjectCount() {
        return mSize;
    }

    public int getMinimumSize() {
        return mMinSize;
    }

    public int getMaximumSize() {
        return mMaxSize;
    }

    public int getIncrementSize() {
        return mIncrementSize;
    }

    public long getCompactionDelayMs() {
        return mCompactionDelayMs;
    }

    /**
     * Get the total size of the array backing the pool.  This will always be >= getPooledObjectCount
     *
     * @return the pool size
     */
    public int getPoolSize() {
        return mPool.length;
    }

    @SuppressWarnings("unchecked")
    private void resizePool(int newSize) {
        T[] newArr = (T[]) Array.newInstance(mClazz, newSize);
        System.arraycopy(mPool, 0, newArr, 0, Math.min(mPool.length, newSize));
        mPool = newArr;
        mSize = Math.min(mSize, newSize);
    }
}
